import {Injectable, Logger} from "@nestjs/common";
import {Neo4jDao} from "../common/database/neo4j.dao";
import {
    AUTHOR,
    BELONGS_TO,
    BOOK, HAS_LABEL,
    LABEL,
    TYPE,
    WRITTEN_BY
} from "../../common/constants/neo4jNode";
import {Neo4jService} from "nest-neo4j/dist";
import {arrayDeduplication, flattenArray} from "../../utils/utils";
import {USERPROFILE} from "../../common/constants/redisKey";
import {RedisService} from "../common/database/redis/redis.service";
import {UserActionService} from "../common/userAction/userAction.service";
import {UserActionDocument} from "../common/userAction/userAction.schema";
import {UserActions} from "../../common/enums/userActions";
import {UserProfileDto} from "./recommend.dto";

/**
 * @Description: 实时推荐模块
 * @author zhaojiuyi
 * @date 2023/4/19
 */

interface ObjectConfig {
    [key: string]: number
}

interface BookConfig {
    id: string
    name: string
    label: string[]
    type: string
    author: string
    score: number

    [key: string]: any
}

type NodeConfig = [string, string, number] // [节点名称，属性名，权重]

@Injectable()
export class RecommendRealTimeService {

    private readonly logger = new Logger(RecommendRealTimeService.name);

    constructor(
        private readonly neo4jDao: Neo4jDao,
        private readonly neo4jService: Neo4jService,
        private readonly redisService: RedisService,
        private readonly userActionService: UserActionService
    ) {
    }

    /**
     * @Description: 推荐图书,根据用户画像与知识图谱
     * @author zhaojiuyi
     * @date 2023/4/11
     * @param {string} userId 用户id
     * @param {number} size 数据条数
     * @param userProfile {UserProfileDto|null} 前端传递过来的用户喜好信息
     */
    async getRecommendBookListComprehensive(userId: string, size: number, userProfile:UserProfileDto): Promise<any[]> {
        let preferenceNodeList: NodeConfig[] = []
        if (Object.keys(userProfile).length) {
            for (const key in userProfile) {
                let nodeName:string
                switch (key) {
                    case "labels":
                        nodeName = LABEL
                        break
                    case "types":
                        nodeName = TYPE
                        break
                    default:
                        nodeName = AUTHOR
                        break
                }
                for (const item of userProfile[key]) {
                    preferenceNodeList.push([nodeName, item, 1])
                }
            }
        } else {
            preferenceNodeList = await this.getUserProfile(userId) as NodeConfig[]
            /* 如果用户画像长度为0，说明用户没有历史操作记录，随机返回size条数据 */
            if (!preferenceNodeList.length) return await this.getRandomBookList(size)
        }
        let resultList = []

        /* 取出用户最感兴趣的5个属性，与知识图谱进行匹配 */
        const nodeArray = preferenceNodeList.slice(0, 5)
        const allWeight = nodeArray.reduce((weight: number, item) => (weight + item[2]), 0)
        for (const node of nodeArray) {
            let [label, name, value] = node
            let tempList: any[]
            switch (label) {
                case AUTHOR:
                    tempList = sortBookByRule(await this.getBookListByAuthor(name), "score", true)
                    break
                case LABEL:
                    tempList = sortBookByRule(await this.getBookListByLabel(name), "score", true)
                    break
                case TYPE:
                    tempList = sortBookByRule(await this.getBookListByType(name), "score", true)
                    break
            }
            /* 截取数据长度，根据权重占比 */
            tempList = sliceListByWeight(tempList.slice(0, size), allWeight, value)
            resultList = [...resultList, ...tempList]
        }
        return sortBookByRule(arrayDeduplication(resultList, "id"), "score", true).slice(0, size)
    }

    /**
     * @Description: 推荐图书,根据作者，分类，标签等
     * @author zhaojiuyi
     * @date 2023/4/11
     * @param {string} bookId 图书id
     * @param {number} size 数据条数
     */
    async getRecommendBookListByBookId(bookId: string, size: number): Promise<any[]> {
        const author = await this.getAuthorsByBookId(bookId)
        const type = await this.getBookTypeById(bookId)
        const labels = await this.getBookLabelsById(bookId)
        let bookList: any[];

        const bookListByAuthor = await this.getBookListByAuthor(author)
        const bookListByType = await this.getBookListByType(type)
        const bookListByLabels = []
        for (const label of labels) {
            const result = await this.getBookListByLabel(label)
            bookListByLabels.push(result)
        }
        /* 删除重复项 */
        bookList = flattenArray([bookListByAuthor, bookListByLabels, bookListByType])
        bookList = arrayDeduplication(bookList, "id");
        bookList = bookList.filter(item => item.id !== bookId)
        /* 根据评分排序，取出前number个 */
        bookList = bookList.sort((a, b) => b["score"] - a["score"]);
        return bookList.slice(0, size);
    }

    /**
     * @Description: 查询具有 某某些标签 的书籍
     * @author zhaojiuyi
     * @date 2023/4/21
     * @param {string[]} labels 标签列表
     * @param {number} size 数据长度
     */
    async getBookListByLabels(labels: string[], size: number): Promise<any[]> {
        let list = []
        for (const label of labels) {
            const result = await this.getBookListByLabel(label)
            list = [...list, ...result]
        }
        const result = arrayDeduplication(list, "id");
        return result.sort((a, b) => b['score'] - a['score']).slice(0, size)
    }

    /**
     * @Description: 获取用户画像
     * @author zhaojiuyi
     * @date 2023/5/3
     * @param {string} userId
     * @return {Promise<{}>}
     */
    async getUserProfile(userId: string): Promise<NodeConfig[] | boolean> {
        // 这里原本是做缓存，后面考虑到用户画像还是需要实时跟新
        // let result = await this.redisService.getValue<any[]>(`${USERPROFILE}:${userId}`)
        // if (!result?.length) {
        //     result = await this.createUserProfile(userId)
        // }
        // return result
        return await this.createUserProfile(userId)
    }


    /**
     * @Description: 绘制用户画像,并保存到redis
     * @author zhaojiuyi
     * @date 2023/5/3
     * @param {string} userId 用户id
     * @return {Promise<boolean>}
     */
    async createUserProfile(userId: string): Promise<NodeConfig[] | boolean> {
        try {
            /* 查询用户读过的书籍 */
            const readList: BookConfig[] = await this.getBookListByActionOrderByTime(userId);
            /* 绘制用户画像，抽取用户历史阅读数据前100条 */
            const labelConfig = {}
            const authorConfig = {}
            const typeConfig = {}
            for (const book of readList.slice(0, 100)) {
                const {label: labels, type, author} = book
                for (const label of labels) {
                    labelConfig[label] = author[label] ? author[label] + 1 : 1
                }
                authorConfig[author] = authorConfig[author] ? authorConfig[author] + 1 : 1
                typeConfig[type] = typeConfig[type] ? typeConfig[type] + 1 : 1
            }
            const authorList: NodeConfig[] = objectConversionSort(authorConfig, AUTHOR)
            const labelList: NodeConfig[] = objectConversionSort(labelConfig, LABEL)
            const typeList: NodeConfig[] = objectConversionSort(typeConfig, TYPE)
            const preferenceNodeList: NodeConfig[] = sortBookByRule([...authorList, ...labelList, ...typeList], 2, true)
            // 用户画像保存到redis，设置60秒过期
            await this.redisService.setValue(`${USERPROFILE}:${userId}`, preferenceNodeList, 60)
            return preferenceNodeList
        } catch (e) {
            return false
        }
    }

    /**
     * @Description: 随机返回一部分图书
     * @author zhaojiuyi
     * @date 2023/5/5
     */
    async getRandomBookList(size: number): Promise<any[]> {
        const CQL = `MATCH (b:${BOOK}) RETURN b ORDER BY RAND() LIMIT ${size}`
        return await this.neo4jDao.read(CQL, "b")
    }


    /* ------------------------------- 编写一些查询节点的方法，提供给上方的推荐算法调用 -------------------------------- */


    /**
     * @Description: 根据作者名称查询该作者的图书
     * @author zhaojiuyi
     * @date 2023/4/11
     * @param {string} name
     * @return {Array}
     */
    getBookListByAuthor = async (name: string): Promise<any[]> => {
        const result = await this.neo4jDao.getRelationship({
            nodeFrom: BOOK,
            nodeTo: AUTHOR,
            relationship: WRITTEN_BY,
            toWhere: {name}
        });
        return result.a;
    }


    /**
     * @Description: 根据图书id查询图书的作者
     * @author zhaojiuyi
     * @date 2023/4/11
     * @param {string} bookId
     */
    getAuthorsByBookId = async (bookId: string): Promise<string> => {
        const authorResult = await this.neo4jDao.getRelationship({
            fromWhere: {id: bookId},
            nodeFrom: BOOK,
            nodeTo: AUTHOR,
            relationship: WRITTEN_BY
        });
        return authorResult.b.map(item => item['name'])[0];
    }

    /**
     * @Description: 获取用户某种行为(看过，收藏，评分，不喜欢等)的书籍列表
     * @author zhaojiuyi
     * @date 2023/4/11
     * @param {string} userId 用户id
     * @param {UserActions} action 用户活动
     */
    getBookListByActionOrderByTime = async (userId: string, action = undefined): Promise<any[]> => {
        /* 查询用户读过的书籍 */
        const historyBookList: UserActionDocument[] = await this.userActionService.find({userId, action})
        /* 数组去重 */
        const tempList: UserActionDocument[] = arrayDeduplication(historyBookList.reverse(), "bookId")
        const bookIdList = tempList.map(item => item.bookId)
        const resultList: any[] = []
        for (const bookId of bookIdList) {
            const book = await this.neo4jDao.findNode(BOOK, {
                id: bookId
            });
            resultList.push(book[0])
        }
        return resultList
    }


    /**
     * @Description: 查询图书的分类
     * @author zhaojiuyi
     * @date 2023/4/11
     */
    getBookTypeById = async (bookId: string): Promise<string> => {
        const result = await this.neo4jDao.getRelationship({
            nodeFrom: BOOK,
            nodeTo: TYPE,
            relationship: BELONGS_TO,
            fromWhere: {
                id: bookId
            }
        });
        return result.b.map(item => item['name'])[0];
    }

    /**
     * @Description: 查询属于某一分类的图书
     * @author zhaojiuyi
     * @date 2023/4/11
     */
    getBookListByType = async (typeName: string): Promise<any[]> => {
        const result = await this.neo4jDao.getRelationship({
            nodeFrom: BOOK,
            nodeTo: TYPE,
            toWhere: {name: typeName},
            relationship: BELONGS_TO
        });
        return result.a;
    }

    /**
     * @Description: 查询图书的标签
     * @author zhaojiuyi
     * @date 2023/4/11
     * @return {string[]} 返回由label组成的数组
     */
    getBookLabelsById = async (bookId: string): Promise<string[]> => {
        const result = await this.neo4jDao.getRelationship({
            nodeFrom: BOOK,
            nodeTo: LABEL,
            relationship: HAS_LABEL,
            fromWhere: {
                id: bookId
            }
        });
        return result.b.map(item => item['name']);
    }

    /**
     * @Description: 查询具有某个标签的图书
     * @author zhaojiuyi
     * @date 2023/4/11
     * @return {any[]} 返回由图书组成的数组
     */
    getBookListByLabel = async (labelName: string): Promise<any[]> => {
        const result = await this.neo4jDao.getRelationship({
            nodeFrom: BOOK,
            nodeTo: LABEL,
            relationship: HAS_LABEL,
            toWhere: {
                name: labelName
            }
        });
        return result.a;
    }

}


/* 工具方法，根据指定规则排序图书列表 */
function sortBookByRule(bookList: any[], rule: string | number, desc?: boolean) {
    let tempDesc = desc ? 1 : -1
    return bookList.sort((a: any, b: any) => (b[rule] - a[rule]) * tempDesc)
}

/* 工具方法，用于将对象转换成元组 元组格式 [节点，名称，权重] */
function objectConversionSort(obj: ObjectConfig, label: string): NodeConfig[] {
    return Object.entries(obj).map(item => [label, ...item])
}

/* 工具方法，根据权重占比筛选结果数组，根据评分排序 */
function sliceListByWeight(bookList: any[], allWeight: number, nodeWeight: number): any[] {
    if (bookList.length <= nodeWeight) return bookList.sort()
    const size = Math.floor(nodeWeight / allWeight * bookList.length)
    return bookList.sort((a: any, b: any) => a['score'] - b['score']).slice(size)
}

